<?php
namespace app\extensions\lib;
use app\exception\ApiException;

class RedisLock
{
    protected $table = null;
    protected $prefix = 'redis_com_lock_';
    public $conn = null;

    public function __construct(array $attributes = [])
    {
        $this->conn = \support\Redis::connection()->client();
    }


    /**
     * 加redis锁
     *
     * @param string $key 键值
     * @param  int $ttl 锁的过期时间
     * @param  int $keep 等待秒数
     * @return string 成功时返回锁标记，用于释放锁
     * @throws Cola_Exception
     */
    public function lock($key, $ttl = 30, $keep = 3)
    {
        $i = $keep * 5;
        while ($i--) {
            if ($lockMark = $this->getLock($key, $ttl)) {
                return $lockMark;
            }

            usleep(200000);
        }

        if ($i < 0) {
            new ApiException( '获取锁失败.key: {$key}',403);
        }
    }

    /**
     * 等待锁释放，（不设置锁，只等待）
     *
     * @param string $key 锁名
     * @param int $keep 等待秒数
     *
     * @throws Cola_Exception
     *
     * @author suika
     */
    public function waitLock($key, $keep = 3)
    {
        $i = $keep * 5;
        while ($i--) {
            if ($this->conn->get($this->prefix . $key) === false) {
                return;
            }

            usleep(200000);
        }
        new ApiException( '等待锁时间超时.key: {$key}',403);
    }

    /**
     * 通过基于watch方法的cas来执行redis逻辑
     *
     * 当在执行$call时，$key的值被改变，会导致$call执行失败并重新执行，这保证了$call方法的原子性
     *
     * @param string|array $key 需要监听
     * @param callable $call 需要执行的原子性redis操作逻辑,****逻辑必须返回exec的结果，不然会进入死循环****
     *
     * @return mixed
     *
     * @author suika （乐观锁）
     * @throws Throwable
     */
    public function casWatch($key, callable $call)
    {
        $this->conn->watch($key);

        do {
            try {
                $res = $call($this->conn, $key);
            } catch (\Throwable $e) {
                $this->conn->unwatch();
                throw $e;
            }
        } while ($res === false);

        return $res;
    }

    /**
     * 解redis锁
     * @param string $key
     * @param string $lockMark 加锁时使用的锁标记
     */
    public function unlock($key, $lockMark)
    {
        $lua = "if redis.call('get', KEYS[1]) == ARGV[1] then 
				    return redis.call('del', KEYS[1])
                else 
                    return 0 
                end";
        $this->conn->eval($lua, array($this->prefix . $key, $lockMark), 1);
    }

    private function getLock($key, $ttl)
    {
        //锁标记
        $lockMark = uniqid();

        // 启用事务，避免设置了key之后没有设置有效期
        $no_lock = $this->conn->multi()->setnx($this->prefix . $key, $lockMark)->expire($this->prefix . $key, $ttl)->exec();

        if (isset($no_lock[1]) && $no_lock[0] == 1 && $no_lock[1] == 1) {
            return $lockMark;
        } else {
            return false;
        }
    }

    /**
     * 自动解锁的加锁逻辑
     *
     * @param string $key 锁的key
     * @param callable $action 需要执行的逻辑
     *
     * @return mixed（悲观锁）
     * @throws Cola_Exception
     * @throws Throwable
     *
     * @author suika
     */
    public function actionLock($key, callable $action)
    {
        $lockMark = $this->lock($key);
        try {
            $rs = $action();
            $this->unlock($key, $lockMark);
            return $rs;
        } catch (\Throwable $e) {
            $this->unlock($key, $lockMark);
            throw $e;
        }
    }

    public function __call($method, $args)
    {
        return $this->conn->$method(...$args);

    }
}